package parsing;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import exceptions.TokenException;
import expressions.Expression;

/**
 * An ExpressionLibrary loads all the expression classes defined in a package.
 * 
 * The package may contain non-expression classes as well; to be identified,
 * each expression class must contain an @Token annotation.
 * 
 * Each expression class must either have a constructor that takes a string --
 * the token it's made from -- as an argument, or a default constructor with no
 * arguments.
 * 
 * @author Misha
 */
public class ExpressionLibrary
{
    private static final String CLASS_EXTENSION = ".class";
    private static final String START_PATH = "bin/";
    private static final String FILE_SEPARATOR = "/";
    private static final String PACKAGE_SEPARATOR = "\\.";
    private static final String PACKAGE_SEPARATOR_NONREGEX = ".";

    Map<Integer, List<Class<?>>> myExpressions;

    List<String> mySeparators;

    /**
     * Loads all the expressions defined in a package.
     * 
     * @param packageName
     */
    public ExpressionLibrary(String packageName)
    {
        myExpressions = new TreeMap<Integer, List<Class<?>>>();
        mySeparators = new ArrayList<String>();

        String formatName = START_PATH
                + packageName.replaceAll(PACKAGE_SEPARATOR, FILE_SEPARATOR);

        File packageDirectory = new File(formatName);

        for (File f : packageDirectory.listFiles())
        {
            String fileName = f.getName();

            if (fileName.endsWith(CLASS_EXTENSION))
            {
                String className = packageName
                        + PACKAGE_SEPARATOR_NONREGEX
                        + fileName.substring(0, fileName.length()
                                - CLASS_EXTENSION.length());

                try
                {
                    Class<?> aClass = Class.forName(className);

                    if (aClass.isAnnotationPresent(Token.class))
                    {
                        int order = 0;

                        if (aClass.isAnnotationPresent(Delay.class))
                            order = aClass.getAnnotation(Delay.class).order();

                        if (!myExpressions.containsKey(order))
                            myExpressions.put(order, new ArrayList<Class<?>>());

                        myExpressions.get(order).add(aClass);

                        if (aClass.isAnnotationPresent(Separates.class))
                        {
                            mySeparators.add(aClass.getAnnotation(Token.class)
                                    .regex());
                        }
                    }
                }
                catch (ClassNotFoundException e)
                {
                    /*
                     * should never happen, but I guess the thing to do is go on
                     * to the next file and act like nothing happened
                     */
                }

            }
        }

    }

    /**
     * Turns a String expression into a list of tokens.
     * 
     * @return <T> The type that the created Expressions work with.
     * @param infix
     *            The string to tokenize.
     * @return A list of tokenized Expressions.
     */
    @SuppressWarnings("unchecked")
    public <T> List<Expression<T>> tokenize(String infix)
    {
        for (String regex : mySeparators)
        {
            infix = infix.replaceAll(regex, " $0 ");
        }

        String[] parts = infix.split("\\s");

        List<Expression<T>> result = new ArrayList<Expression<T>>();

        for (String part : parts)
        {
            if (part.equals(""))
                continue;

            result.add((Expression<T>) matchToken(part));
        }

        return result;
    }

    /**
     * Tries to match a token to an Expression class that this library knows
     * about.
     * 
     * @param token
     *            The string to be matched.
     * @return An Expression<?> created from the string.
     */
    private Expression<?> matchToken(String token)
    {
        for (int order = 0; myExpressions.containsKey(order); order++)
        {
            for (Class<?> exprClass : myExpressions.get(order))
            {
                if (token.matches(exprClass.getAnnotation(Token.class).regex()))
                {
                    return makeExpression(exprClass, token);
                }
            }
        }

        throw new TokenException(token);
    }

    /**
     * Uses reflection to construct an Expression of a given class, from a token
     * String.
     * 
     * @param exprClass
     *            the class that the resulting expression should be.
     * @param token
     *            The token that represented this expression in the input.
     * @return The constructed Expression, or null in the case of failure.
     */
    private Expression<?> makeExpression(Class<?> exprClass, String token)
    {
        try
        {
            // first try to construct expression with the token string
            return (Expression<?>) exprClass.getConstructor(String.class)
                    .newInstance(token);
        }
        catch (Exception e)
        {
            try
            {
                // next try the default constructor
                return (Expression<?>) exprClass.getConstructor().newInstance();
            }
            catch (Exception e2)
            {
                return null;
            }
        }
    }
}
